
一个通道可以在LLVM IR上执行任意复杂度的转换。为了说明添加新通道的机制，我们添加一个执行简单检测的通道。

为了研究程序的性能，了解函数被调用的频率和运行的时间是很有趣的。收集这些数据的一种方法是在每个函数中插入计数器，这个过程称为插装。我们将编写一个简单的插装通道，在每个函数的入口和每个退出点插入一个特殊的函数调用，这些函数收集计时信息并将其写入文件。因此，可以创建一个非常基本的分析器，将其命名为“穷人”(poor people)分析器，或者简而言之——ppprofiler。我们将开发新通道，以便其可以作为一个独立的插件使用，或者作为一个插件添加到LLVM源代码树中。最后，我们将了解如何将LLVM自带的通道集成到框架中。

\mySubsubsection{7.3.1.}{将ppprofiler作为插件进行开发}

本节中，将了解如何在LLVM树中创建一个新通道作为插件。新通道的目标是在函数的入口处插入对\_\_ppp\_enter()函数的调用，并在每个返回指令之前插入对\_\_ppp\_exit()函数的调用。只有当前函数的名称作为参数传递，这些函数的实现可以计算调用的次数，并测量所使用的时间。我们将在本章的最后实现这个运行时支持，并将研究如何开发通道。

我们将源代码存储在ppprofile.cpp文件中。遵循以下步骤:

\begin{enumerate}
\item
首先，包含一些文件:

\begin{cpp}
#include "llvm/ADT/Statistic.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/PassManager.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Support/Debug.h"
\end{cpp}

\item
为了缩短源代码，告诉编译器正在使用llvm命名空间:

\begin{cpp}
using namespace llvm;
\end{cpp}

\item
LLVM的内置调试基础结构要求定义一个调试类型，它是一个字符串。这个字符串随后显示在打印的统计信息中:

\begin{cpp}
#define DEBUG_TYPE "ppprofiler"
\end{cpp}

\item
接下来，将使用ALWAYS\_ENABLED\_STATISTIC宏定义一个计数器变量。第一个参数是计数器变量的名称，而第二个参数是将在统计中打印的文本:

\begin{cpp}
ALWAYS_ENABLED_STATISTIC(
    NumOfFunc, "Number of instrumented functions.");
\end{cpp}

\begin{myNotic}{Note}
可以使用两个宏来定义计数器变量。若使用STATISTIC宏，则只有在启用断言的情况下，或者在CMake命令行中将LLVM\_FORCE\_ENABLE\_STATS设置为ON时，才会在调试构建中收集统计值。若使用ALWAYS\_ENABLED\_STATISTIC宏，则总是收集统计值。但使用-stats命令行选项输出统计信息只适用于前两种方法。若需要，可以通过调用llvm::PrintStatistics(llvm::raw\_ostream)函数打印收集的统计信息。
\end{myNotic}

\item
接下来，必须在匿名命名空间中声明通道类，这个类继承自PassInfoMixin模板。这个模板只添加了一些样板代码，比如name()方法，不用于确定传递的类型，run()方法在执行传递时由LLVM调用，还需要一个名为instrument()的辅助方法:

\begin{cpp}
namespace {
class PPProfilerIRPass
: public llvm::PassInfoMixin<PPProfilerIRPass> {
public:
    llvm::PreservedAnalyses
    run(llvm::Module &M, llvm::ModuleAnalysisManager &AM);

private:
    void instrument(llvm::Function &F,
                    llvm::Function *EnterFn,
                    llvm::Function *ExitFn);
};
}
\end{cpp}

\item
现在，来定义如何插装函数。除了要检测的函数外，还要通道要调用的函数:

\begin{cpp}
void PPProfilerIRPass::instrument(llvm::Function &F,
                                  Function *EnterFn,
                                  Function *ExitFn) {
\end{cpp}

\item
在函数内部，更新统计计数器:

\begin{cpp}
    ++NumOfFunc;
\end{cpp}

\item
为了方便地插入IR代码，需要IRBuilder类的一个实例。我们把它设置为第一个基本块，这是函数的入口块:

\begin{cpp}
    IRBuilder<> Builder(&*F.getEntryBlock().begin());
\end{cpp}

\item
现在有了构建器，可以插入一个全局常量来保存检测函数的名称:

\begin{cpp}
    GlobalVariable *FnName =
        Builder.CreateGlobalString(F.getName());
\end{cpp}

\item
接下来，将插入对\_\_ppp\_enter()函数的调用，并将名称作为参数传递:

\begin{cpp}
    Builder.CreateCall(EnterFn->getFunctionType(), EnterFn,
                        {FnName});
\end{cpp}

\item
要调用\_\_ppp\_exit()函数，必须定位所有返回指令。SetInsertionPoint()函数设置的插入点在作为参数传递的指令之前，所以可以在该点插入调用:

\begin{cpp}
    for (BasicBlock &BB : F) {
        for (Instruction &Inst : BB) {
            if (Inst.getOpcode() == Instruction::Ret) {
                Builder.SetInsertPoint(&Inst);
                Builder.CreateCall(ExitFn->getFunctionType(),
                                   ExitFn, {FnName});
            }
        }
    }
}
\end{cpp}

\item
接下来，将实现run()方法。LLVM通道所工作的模块和一个分析管理器，若需要，可以从中请求分析结果:

\begin{cpp}
PreservedAnalyses
PPProfilerIRPass::run(Module &M,
                      ModuleAnalysisManager &AM) {
\end{cpp}

\item
这里有一个小麻烦:若在实现的运行时模块检测到\_\_ppp\_enter()和\_\_ppp\_exit()函数，则创建了一个无限递归，就会遇到麻烦。为了避免这种情况，若定义了其中一个函数，必须什么都不做:

\begin{cpp}
    if (M.getFunction("__ppp_enter") ||
        M.getFunction("__ppp_exit")) {
            return PreservedAnalyses::all();
    }
\end{cpp}

\item
现在，我们准备声明函数。首先，创建函数类型，然后是函数体:

\begin{cpp}
    Type *VoidTy = Type::getVoidTy(M.getContext());
    PointerType *PtrTy =
        PointerType::getUnqual(M.getContext());
    FunctionType *EnterExitFty =
        FunctionType::get(VoidTy, {PtrTy}, false);
    Function *EnterFn = Function::Create(
        EnterExitFty, GlobalValue::ExternalLinkage,
        "__ppp_enter", M);
    Function *ExitFn = Function::Create(
        EnterExitFty, GlobalValue::ExternalLinkage,
        "__ppp_exit", M);
\end{cpp}

\item
现在需要做的就是循环遍历模块的所有函数，并通过调用instrument()方法检测找到的函数。需要忽略函数声明，它们只是原型。也可能存在没有名称的函数，这与我们的方法不兼容，就会过滤掉这些函数:

\begin{cpp}
    for (auto &F : M.functions()) {
        if (!F.isDeclaration() && F.hasName())
            instrument(F, EnterFn, ExitFn);
    }
\end{cpp}

\item
最后，必须声明，我们没有保留任何分析。这很可能过于悲观，但这样做是出于安全考虑:

\begin{cpp}
    return PreservedAnalyses::none();
}
\end{cpp}

新通道的功能现在已经实现了。为了能够使用我们的通道，需要用PassBuilder对象注册。这可以通过两种方式实现:静态或动态。若插件静态链接，则需要提供一个名为get<PluginName>PluginInfo()的函数。要使用动态链接，需要提供llvmGetPassPluginInfo()函数。这两种情况下，都会返回一个PassPluginLibraryInfo结构体的实例，提供了关于插件的一些基本信息。最重要的是，结构体包含一个指向注册通道的函数指针。现在，让我们把它添加到源文件中。

\item
RegisterCB()函数中，注册了一个Lambda函数，该函数将在解析通道流水线字符串时调用。若通道的名称是ppprofiler，将通道添加到模块通道管理器中。这些回调将在下一节中展开:

\begin{cpp}
void RegisterCB(PassBuilder &PB) {
    PB.registerPipelineParsingCallback(
        [](StringRef Name, ModulePassManager &MPM,
           ArrayRef<PassBuilder::PipelineElement>) {
            if (Name == "ppprofiler") {
                MPM.addPass(PPProfilerIRPass());
                return true;
            }
            return false;
        });
}
\end{cpp}

\item
getPPProfilerPluginInfo()函数在静态链接插件时调用，会返回一些关于插件的基本信息:

\begin{cpp}
llvm::PassPluginLibraryInfo getPPProfilerPluginInfo() {
    return {LLVM_PLUGIN_API_VERSION, "PPProfiler", "v0.1",
        RegisterCB};
}
\end{cpp}

\item
若插件动态链接，则在加载插件时调用llvmGetPassPluginInfo()函数。但当将此代码静态地链接到工具中时，可能会出现链接器错误，因为该函数可能在多个源文件中定义。解决方案是使用宏来保护函数:

\begin{cpp}
#ifndef LLVM_PPPROFILER_LINK_INTO_TOOLS
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
    return getPPProfilerPluginInfo();
}
#endif
\end{cpp}

\end{enumerate}

这样，我们就实现了通道插件。在了解如何使用新插件之前，让我们检查一下，若想要将通道插件添加到LLVM源代码树中，需要更改哪些内容。

\mySubsubsection{7.3.2.}{将通道添加到LLVM源代码树中}

例如，若计划将新通道与预编译的clang一起使用，则将其实现为插件是有用的。另一方面，若编写自己的编译器，就有很好的理由将新通道直接添加到LLVM源代码树中。有两种不同的方式可以做到这一点——作为一个插件和作为一个集成通道。插件方法的修改会更少一些。

\mySamllsection{利用LLVM源代码树中的插件机制}

LLVM IR上执行转换的传递源位于llvm-project/llvm/lib/Transforms目录中。这个目录中，创建一个名为PPProfiler的新目录，并将源文件PPProfiler.cpp复制到其中，不需要对源代码进行任何更改!

为了将新插件集成到构建系统中，创建一个名为CMakeLists.txt的文件，内容如下:

\begin{cmake}
add_llvm_pass_plugin(PPProfiler PPProfiler.cpp)
\end{cmake}

最后，在父目录下的CMakeLists.txt文件中，需要通过添加以下行来包含新的源目录:

\begin{cmake}
add_subdirectory(PPProfiler)
\end{cmake}

现在，已经准备好构建添加了PPProfiler的LLVM。进入LLVM的构建目录，手动运行Ninja:

\begin{shell}
$ ninja install
\end{shell}

CMake将检测到构建描述中的变化，并重新运行配置步骤。这里，会看到一行输出:

\begin{shell}
-- Registering PPProfiler as a pass plugin (static build: OFF)
\end{shell}

这说明该插件已检测到并已构建为动态库。安装完成后，在<install directory>/lib目录下，将找到动态库PPProfiler.so。

目前为止，与前一节的通道插件的唯一区别是，动态库是作为LLVM的一部分安装的，也可以静态地将新插件链接到LLVM工具。要做到这一点，需要重新运行CMake配置并在命令行中添加-DLLVM\_PPPROFILER\_LINK\_INTO\_TOOLS=ON选项。从CMake中查找以下信息以确认已更改的构建选项:

\begin{shell}
-- Registering PPProfiler as a pass plugin (static build: ON)
\end{shell}

重新编译安装LLVM后，发生如下变化:

\begin{itemize}
\item
该插件编译到静态库libPPProfiler.a中，并且该库安装在<install directory>/lib目录下。

\item
LLVM工具(如opt)会链接到该库。

\item
该插件注册为扩展，可以检查<install directory>/include/llvm/Support/Extension.def文件现在包含以下行:

\begin{shell}
HANDLE_EXTENSION(PPProfiler)
\end{shell}
\end{itemize}

此外，所有支持此扩展机制的工具都将获得新的通道。创建优化流水线一节中，将了解如何在编译器中执行此操作。

因为新的源文件位于单独的目录中，所以这种方法工作得很好，并且只修改一个现有文件。若试图保持修改后的LLVM源树与主存储库同步，这将最小化合并冲突。

某些情况下，将新通道添加为插件并不是最好的方法，LLVM提供的通道使用不同的方式进行注册。若开发了一个新通道，并将其添加到LLVM，并且LLVM社区接受了您的贡献，可以使用注册机制。

\mySamllsection{将通道集成到注册表中}

为了将新通道完全集成到LLVM中，需要对插件的源代码进行稍微不同的结构调整。这样做的主要原因是从通道注册表调用通道类的构造函数，这需要将类接口放入头文件中。

与之前一样，必须将新通道放入LLVM的Transforms组件中。通过创建lvm-project/llvm/include/llvm/Transforms/PPProfiler/PPProfiler.h头文件开始实现。该文件的内容是类定义，将其放入LLVM的命名空间中。无需其他更改:

\begin{cpp}
#ifndef LLVM_TRANSFORMS_PPPROFILER_PPPROFILER_H
#define LLVM_TRANSFORMS_PPPROFILER_PPPROFILER_H

#include "llvm/IR/PassManager.h"

namespace llvm {
class PPProfilerIRPass
    : public llvm::PassInfoMixin<PPProfilerIRPass> {
public:
    llvm::PreservedAnalyses
    run(llvm::Module &M, llvm::ModuleAnalysisManager &AM);
private:
    void instrument(llvm::Function &F,
                    llvm::Function *EnterFn,
                    llvm::Function *ExitFn);
};
} // namespace llvm
#endif
\end{cpp}

接下来，将通道插件的源文件PPProfiler.cpp复制到新目录llvmproject/llvm/lib/Transforms/PPProfiler中。该文件需要按照如下方式更新:

\begin{enumerate}
\item
由于类定义现在位于头文件中，因此必须从该文件中删除类定义。在顶部，添加\#include:

\begin{cpp}
#include "llvm/Transforms/PPProfiler/PPProfiler.h"
\end{cpp}

\item
必须删除llvmGetPassPluginInfo()函数，因为通道没有内置到动态库中。
\end{enumerate}

和前面一样，还需要为构建提供一个CMakeLists.txt文件，必须将新通道声明为一个新组件:

\begin{cmake}
add_llvm_component_library(LLVMPPProfiler
    PPProfiler.cpp

    LINK_COMPONENTS
    Core
    Support
)
\end{cmake}

之后，与前一节一样，需要通过向父目录下的CMakeLists.txt添加以下行来包含新的源目录:

\begin{cmake}
add_subdirectory(PPProfiler)
\end{cmake}

LLVM内部，可用的通道保存在llvm/lib/Passes/PassRegistry.def数据库文件中，需要更新这个文件。新通道是一个模块通道，因此需要在文件中搜索定义模块通道部分，例如，搜索MODULE\_PASS宏。在这个部分中，添加以下行:

\begin{shell}
MODULE_PASS("ppprofiler", PPProfilerIRPass())
\end{shell}

该数据库文件在llvm/lib/Passes/PassBuilder.cpp类中使用。这个文件需要包含新的头文件:

\begin{cpp}
#include "llvm/Transforms/PPProfiler/PPProfiler.h"
\end{cpp}

这些都是基于新通道的插件版本所必需的源码更改。

由于创建了一个新的LLVM组件，还需要在llvm/lib/Passes/CMakeLists.txt文件中添加链接依赖项。在LINK\_COMPONENTS关键字下，需要添加一行新组件的名称:

\begin{shell}
PPProfiler
\end{shell}

现在，可以开始构建和安装LLVM了，所有LLVM工具都可以使用新的通道ppprofiler。它已经编译到libLLVMPPProfiler.a库中，并且可以作为PPProfiler组件在构建系统中使用。

我们已经讨论了如何创建一个新的通道。在下一节中，将研究如何使用ppprofiler通道。









